package
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.TimerEvent;
	import flash.net.SharedObject;
	import flash.utils.Timer;
	
	import mx.collections.ArrayCollection;
	import mx.controls.Alert;
	import mx.core.FlexGlobals;
	import mx.events.CloseEvent;
	import mx.events.MoveEvent;
	import mx.events.ResizeEvent;
	import mx.rpc.CallResponder;
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	
	import services.jira.Jira;
	
	import valueObjects.RemoteFieldValue;

	////////////////////////////////////////////////////////////////////////////////
	//
	// Initialization and singleton management.
	//
	
	public class JTStateManager extends EventDispatcher
	{
		public function JTStateManager(enforcer:SingletonEnforcer) { ; }
		private static var m_mgr:JTStateManager;
		
		public static function getInstance():JTStateManager
		{
			if( m_mgr == null) {
				m_mgr = new JTStateManager(new SingletonEnforcer());
			}
			return m_mgr;
		}

		//
		// n.b. Screen state and login information is persisted in a local shared object.
		//
		
		private var m_sharedObject:SharedObject;
		
		public function initialize():void
		{
			try
			{
				var m_sharedObjectName:String = "JiraToolLocalApplicationState";
				m_sharedObject = SharedObject.getLocal(m_sharedObjectName);
			}
			catch(e:Error)
			{
				// No prob if there's no cached info.
			}
			
			loadPersistedState();
			positionWindow();

			FlexGlobals.topLevelApplication.stage.nativeWindow.addEventListener(MoveEvent.MOVE, MoveOrResize);
			FlexGlobals.topLevelApplication.stage.nativeWindow.addEventListener(ResizeEvent.RESIZE, MoveOrResize);

			m_jiraService.showBusyCursor = true;
			m_jiraService.addEventListener(FaultEvent.FAULT, defaultJiraFault);
		}

		// Utilities for logging web service information and broadcasting model change events.
		
		private function displayMessage(msg:String):void
		{
			FlexGlobals.topLevelApplication.action_area.displayMessage(msg);
		}
		
		private function dispatchChangeEvent():void
		{
			var changeEvent:Event = new Event(Event.CHANGE);
			this.dispatchEvent(changeEvent);			
		}

		////////////////////////////////////////////////////////////////////////////////
		//
		// The Jira web service instance.
		//
		// FIXME: If the SOAP service faults because there's no network available, any subsequent operation hangs
		// forever regardless of whether or not the network was restored.  I haven't found a way around this
		// unfortunately...  you have to quite and restart JiraTool.
		//
		
		private var m_jiraService:Jira = new Jira;
		public function get jiraService():Jira { return m_jiraService; }
		private function defaultJiraFault(event:FaultEvent):void
		{
			Alert.show(event.fault.faultString);
			displayMessage( "Jira Fault Details: " + event.fault.faultDetail + "\n");

			m_operationInProgress = false;
			m_loginInProgress = false;
			dispatchChangeEvent();
		}

		////////////////////////////////////////////////////////////////////////////////
		//
		// The managed data for the application.
		//

		// Window state.  Persisted, but not considered part of the application data model.
		
		private var m_lastWidth:int = 800;
		private var m_lastHeight:int = 600;
		private var m_lastX:int = 20;
		private var m_lastY:int = 20;

		// The persisted data considered part of the application data model.
		
		private var m_saveCredentials:Boolean = false;
		private var m_loginString:String;
		private var m_passwordString:String;

		// This is the current state of the "action area".
		private var m_actionAreaState:String = "summary";
		
		// This is the current Jira project we're working with.
		private var m_jiraProject:String = "FBL";

		////////////////////////////////////////////////////////////////////////////////
		//
		// The non-persisted data considered part of the application data model.
		//

		private var m_operationInProgress:Boolean = false;
		
		private var m_loginInProgress:Boolean = false;
		private var m_loginToken:String;

		// This is an array of objects, one per project.  Each object contains a "name" and an array a "milestones".
		// This is used to populate the project selector button bar and to create the issues query.
		private var m_releasesAndMilestones:Array = new Array;
		private var m_currentReleaseIndex:uint = NaN;
		
		// This is an array of objects, one per component.  Each object contains a "name" and an array of "workers".
		// Each worker in the array of "workers" has a "username" and a "days_remaining" total.
		// This is used to populate the summary view in the action area.
		private var m_workItemsByFeature:Array = new Array;

		// This is an array of objects, one per top-level "Task" grouping.  Each object contains a "name" and an
		// array of "work_items".  Each work item in the array has a "key", a "summary", an "assignee", and a "days"
		// remaining.  This is used to populate the work items views in the action area.
		private var m_workItemsByWorkItemGroup:Array = new Array;

		private var m_workItemGroupsQueryTemplate:String = "project = %%PROJ%% AND issuetype = Task AND status != Closed AND (Milestone in (%%MILESTONES%%) OR Milestone is EMPTY)";
		private var m_workItemsGroupsQuery:String;

		private var m_workItemsPerGroupQueryTemplate:String = "project = %%PROJ%% AND issuetype = \"Misc. Sub-Task\" AND parent = %%PARENT%% AND status != Closed AND (Milestone in (%%MILESTONES%%) OR Milestone is EMPTY)";
		private var m_workItemsPerGroupQuery:String;

		private var m_maxQueryResults:Number = 1000;

		//
		// There are several queries to Jira the are necessary to build up the application data.
		// Those queries have a well-defined ordering as follows:
		//
		//     Login -> GetConfigData -> GetComponentList -> GetWorkItemGroups -> GetWorkItems
		//
		// It's valid to enter this sequence at each of the following points for the reasons
		// described.  Once you enter at a defined point, though, the sequence will run all
		// the way to the end until either a fault is encountered or the final result is processed.
		// 
		// Here are the valid entry points and reasons:
		//
		//     Login:
		//         Server operations start here when the application is first logging in to Jira.
		//
		//     GetConfigData:
		//         Server operations start here when the user has changed the Jira database in the
		//         application settings UI which results in a call to the setter for jiraProject.
		//         The user does this when they want to look at the work items data for a different
		//         product (i.e. Flex SDK vs. Flash Builder).
		//
		//     GetWorkItemGroups:
		//         Server operations start here when the user has changed the release they want to
		//         see data for (i.e. "Mega" vs. "Ultra") which results in a call to the setter for
		//         currentReleaseIndex, or if they want to reload the current data from the server
		//         which they do from the "Refresh" button in the JTActionBar.
		//
		// Neither GetComponentList nor GetWorkItems should be used as entry points into the
		// server communication chain.
		//
		
		private var m_projectDataLoaded:Boolean = false;
		private var m_workItemQueriesOutstanding:uint = 0;		
		private var m_workItemQueriesParentTokenMap:Array = new Array;

		////////////////////////////////////////////////////////////////////////////////
		
		private function resetVarsForNewJiraProject():void
		{
			m_actionAreaState = "summary";

			m_releasesAndMilestones.length = 0;
			m_workItemsByFeature.length = 0;
			
			clearPendingUpdates();
			m_currentReleaseIndex = NaN;

			resetVarsForNewDefinedRelease();
		}
		
		private function resetVarsForNewDefinedRelease():void
		{
			for( var i:uint = 0; i < m_workItemsByFeature.length; i++ )
				m_workItemsByFeature[i].workers.length = 0;
			
			m_workItemsByWorkItemGroup.length = 0;
			m_workItemQueriesOutstanding = 0;		
			m_workItemQueriesParentTokenMap.length = 0;
			
			m_workItemsGroupsQuery = null;
			m_workItemsPerGroupQuery = null; 

			m_projectDataLoaded = false;
			dispatchChangeEvent();
		}

		////////////////////////////////////////////////////////////////////////////////
		//
		// The persistence management routintes persisted data.
		//

		private function loadPersistedState():void
		{
			if( m_sharedObject != null )
			{
				if( m_sharedObject.data.LastWidth != null )
					m_lastWidth = m_sharedObject.data.LastWidth
				if( m_sharedObject.data.LastHeight != null )
					m_lastHeight = m_sharedObject.data.LastHeight;
				if( m_sharedObject.data.LastXPos != null )
					m_lastX = m_sharedObject.data.LastXPos;
				if( m_sharedObject.data.LastYPos != null )
					m_lastY = m_sharedObject.data.LastYPos;
				if( m_sharedObject.data.SaveCredentials != null )
					m_saveCredentials = m_sharedObject.data.SaveCredentials;
				if( m_sharedObject.data.LoginName != null )
					m_loginString = m_sharedObject.data.LoginName;
				if( m_sharedObject.data.SessionToken != null )
					m_passwordString = decodePassword( m_sharedObject.data.SessionToken );
				if( m_sharedObject.data.ActionAreaState != null )
					m_actionAreaState = m_sharedObject.data.ActionAreaState;
				if( m_sharedObject.data.JiraProject != null )
					m_jiraProject = m_sharedObject.data.JiraProject;
				
				dispatchChangeEvent();
				
				//
				// If we restored a username and password from the persisted data,
				// then go ahead and automatically attempt a login.
				//
				
				if( ( null != m_loginString ) && ( "" != m_loginString ) &&
					( null != m_passwordString ) && ( "" != m_passwordString ) )
				{
					doLogin( m_loginString, m_passwordString );
				}
			}
		}

		// FIXME: The pattern for persisting only the window state could be made clearer.
		
		private function persistState(windowStateOnly:Boolean):void
		{
			if( m_sharedObject != null )
			{
				m_sharedObject.setProperty("LastWidth", m_lastWidth);
				m_sharedObject.setProperty("LastHeight", m_lastHeight );
				m_sharedObject.setProperty("LastXPos", m_lastX );
				m_sharedObject.setProperty("LastYPos", m_lastY );
				
				if( false == windowStateOnly )
				{
					m_sharedObject.setProperty("ActionAreaState", m_actionAreaState );
					m_sharedObject.setProperty("JiraProject", m_jiraProject );
					
					m_sharedObject.setProperty("SaveCredentials", m_saveCredentials );
					if( m_saveCredentials )
					{
						m_sharedObject.setProperty("LoginName", m_loginString );
						m_sharedObject.setProperty("SessionToken", encodePassword( m_passwordString ) );
					}
				}
				
				m_sharedObject.flush();
			}
		}

		////////////////////////////////////////////////////////////////////////////////
		//
		// The window position management routines.
		//

		private function positionWindow():void
		{
			FlexGlobals.topLevelApplication.stage.nativeWindow.x = m_lastX;
			FlexGlobals.topLevelApplication.stage.nativeWindow.y = m_lastY;
			FlexGlobals.topLevelApplication.width = m_lastWidth;
			FlexGlobals.topLevelApplication.height = m_lastHeight;
		}
		
		private var persistTimer:Timer;
		
		private function MoveOrResize(e:Event):void
		{ 	
			m_lastX = FlexGlobals.topLevelApplication.stage.nativeWindow.x;
			m_lastY = FlexGlobals.topLevelApplication.stage.nativeWindow.y;
			m_lastWidth = FlexGlobals.topLevelApplication.width;
			m_lastHeight = FlexGlobals.topLevelApplication.height;
			
			// Avoid hammering on the shared object during a window resize.
			
			if( null == persistTimer )
			{
				persistTimer = new Timer( 3000 );
				persistTimer.repeatCount = 1;
				persistTimer.addEventListener(TimerEvent.TIMER, doDelayedPersist);				
			}
			
			if( false == persistTimer.running )
				persistTimer.start();
		}
		
		private function doDelayedPersist(e:TimerEvent):void { persistState(true); }  
		
		////////////////////////////////////////////////////////////////////////////////
		//
		// Accessors for the data model.
		//

		public function get loginToken():String { return m_loginToken; }
		public function get loginInProgress():Boolean { return m_loginInProgress; }

		public function get definedReleaseCount():uint { return m_releasesAndMilestones.length; }
		public function getDefinedReleaseName(index:uint):String
		{
			if( index < m_releasesAndMilestones.length )
				return new String( m_releasesAndMilestones[index].name );
			
			return( null );
		}
		
		public function get currentReleaseIndex():uint { return m_currentReleaseIndex; }
		public function set currentReleaseIndex(i:uint):void
		{
			// If we're in the middle of loading server data, ignore this request.
			if( m_operationInProgress ) return;
			
			if( m_releasesAndMilestones.length > i )
			{
				m_operationInProgress = true;
				m_currentReleaseIndex = i;
				resetVarsForNewDefinedRelease();
				getWorkItemGroups();
			}
		}

		////////////////////////////////////////////////////////////////////////////////
		//
		// Changing the Jira project is destructive.
		//
		
		private var m_newJiraProject:String;
		public function get jiraProject():String { return m_jiraProject; }
		public function set jiraProject(project:String):void
		{
			// If we're in the middle of loading server data, ignore this request.
			if( m_operationInProgress ) return;

			m_operationInProgress = true;
			m_newJiraProject = project;
			
			if( havePendingUpdates() )
			{
				Alert.show( "You have unsaved updates!\n\nAre you sure you want to switch projects?\n\n",
					"Warning: Unsaved Updates",
					Alert.YES|Alert.CANCEL, 
					null,
					setJiraProjectAlertHandler );
			}
			else
			{
				internalSetJiraProject();
			}
		}

		private function internalSetJiraProject():void
		{
			m_jiraProject = m_newJiraProject;
			m_newJiraProject = null;
			persistState(false);			
			
			resetVarsForNewJiraProject();
			getConfigurationData();			
		}

		private function setJiraProjectAlertHandler(event:CloseEvent):void
		{
			if (event.detail == Alert.YES)
				internalSetJiraProject();
			else
			{
				// Dispatch a change event so that the project edit box reverts back to the current project.
				// m_jiraProject = new String( m_jiraProject );
				m_operationInProgress = false;
				m_newJiraProject = null;
				dispatchChangeEvent();
			}
		}

		////////////////////////////////////////////////////////////////////////////////
		//
		// Reloading from the server is destructive.
		//

		public function reload():void
		{
			// If we're in the middle of loading server data, ignore this request.
			if( m_operationInProgress ) return;

			if( havePendingUpdates() )
			{
				Alert.show( "You have unsaved updates!\n\nAre you sure you want to reload?\n\n",
					"Warning: Unsaved Updates",
					Alert.YES|Alert.CANCEL, 
					null,
					reloadAlertHandler );
			}
			else
			{
				internalReload();
			}
		}

		private function internalReload():void
		{
			// We make reloading explicitly destructive, though normally changing between
			// defined releases within the same project does not clear your pending updates 
			// queue.
			
			clearPendingUpdates();

			m_operationInProgress = true;
			resetVarsForNewDefinedRelease();
			getWorkItemGroups();						
		}
		
		private function reloadAlertHandler(event:CloseEvent):void
		{
			if (event.detail == Alert.YES)
				FlexGlobals.topLevelApplication.exit();
		}

		////////////////////////////////////////////////////////////////////////////////
		//
		// Reloading from the server is destructive.
		//

		public function applicationExit():void
		{
			if( havePendingUpdates() )
			{
				Alert.show( "You have unsaved updates!\n\nAre you sure you want to exit?\n\n",
					        "Warning: Unsaved Updates",
							Alert.YES|Alert.CANCEL, 
							null,
							appExitAlertHandler );
			}
			else
			{
				FlexGlobals.topLevelApplication.exit();
			}
		}
		
		private function appExitAlertHandler(event:CloseEvent):void
		{
			if (event.detail == Alert.YES)
				FlexGlobals.topLevelApplication.exit();
		}

		////////////////////////////////////////////////////////////////////////////////

		public function get workItemsByFeature():Array { return m_workItemsByFeature; }
		public function get workItemsByWorkItemGroup():Array { return m_workItemsByWorkItemGroup; }
		public function get projectDataLoaded():Boolean { return m_projectDataLoaded; }
		
		public function get actionAreaState():String { return m_actionAreaState; }
		public function set actionAreaState(state:String):void
		{
			// Make sure it's a known state that's being requested.
			if( ( state != "summary" ) &&
				( state != "my_tasks" ) &&
				( state != "all_tasks" ) &&
				( state != "messages" ) &&
				( state != "settings" ) )
			{
				return;
			}
			
			m_actionAreaState = state;
			persistState(false);			
			
			dispatchChangeEvent();
		}
		
		public function get saveCredentials():Boolean { return m_saveCredentials; }
		public function set saveCredentials(saveCredentials:Boolean):void
		{
			// n.b. If the caller wants to stop saving the credentials, we have to clear them out
			// of the shared object before we can stop persisting them altogether.
			
			if( false == saveCredentials )
			{
				m_loginString = null;
				m_passwordString = null;
				m_saveCredentials = true;
				persistState(false);
			}

			m_saveCredentials = saveCredentials;
			persistState(false);			

			dispatchChangeEvent();
		}
		
		public function get username():String { return m_loginString; }
		public function set username(un:String):void
		{
			m_loginString = un;
			persistState(false);			
			dispatchChangeEvent();
		}
		
		public function get password():String { return m_passwordString; }
		public function set password(pw:String):void
		{
			m_passwordString = pw;
			persistState(false);	
			dispatchChangeEvent();
		}

		// n.b. This is not encryption.  It is merely a courtesy to the user that we
		// avoid storing their password in flat out plain text.
		
		protected function decodePassword( pw:String ):String
		{
			if( null == pw ) return null;
			
			var charCodes:Array = pw.split(" ");
			var pwText:String = new String;
			
			for( var i:int = 0; i < charCodes.length; i++ )
				pwText = pwText.concat( String.fromCharCode( charCodes[i] ) );
			
			return( pwText );
		}
		
		protected function encodePassword( pw:String ):String
		{
			if( null == pw ) return null;

			var pwCodes:String = new String;
			
			for( var i:int = 0; i < pw.length; i++ ) 
			{
				// n.b. Don't add an extra space character at the end, or when I decode
				// this it will have an extra null character that will caused a SAX
				// parser error during the login attempt.
				
				pwCodes = pwCodes.concat( pw.charCodeAt(i).toString() );
				if( i != (pw.length - 1) )
					pwCodes = pwCodes.concat( " " );
			}
			
			return( pwCodes );
		}

		////////////////////////////////////////////////////////////////////////////////

		public function doLogin(username:String, password:String):void
		{
			// If we're in the middle of loading server data, ignore this request.
			if( m_operationInProgress ) return;

			displayMessage("Initiating login for " + username );
			
			var loginResponder:CallResponder = new CallResponder;

			m_loginToken = null;
			m_loginInProgress = true;
			m_operationInProgress = true;
			dispatchChangeEvent();

			loginResponder.addEventListener(ResultEvent.RESULT, handleLoginResult );
			loginResponder.addEventListener(FaultEvent.FAULT, defaultJiraFault );
			loginResponder.token = jiraService.login( username, password );
		}
		
		protected function handleLoginResult(event:ResultEvent):void
		{
			displayMessage("Received login token: " + event.result.toString());

			m_loginToken = event.result.toString();
			m_loginInProgress = false;

			dispatchChangeEvent();

			// Call the next operation in the loading sequence.
			getConfigurationData();
		}

		////////////////////////////////////////////////////////////////////////////////

		protected function getConfigurationData():void
		{
			displayMessage("Getting configuration data for " + jiraProject );
			
			var configIssue:String = jiraProject.concat("-1");

			var configResponder:CallResponder = new CallResponder;
			configResponder.addEventListener(ResultEvent.RESULT, handleConfigResult );
			configResponder.addEventListener(FaultEvent.FAULT, defaultJiraFault );
			configResponder.token = jiraService.getIssue( m_loginToken, configIssue );
		}

		protected function handleConfigResult(event:ResultEvent):void
		{
			for( var i:uint = 0; i < event.result.customFieldValues.length; i++ )
			{
				if( event.result.customFieldValues[i].customfieldId == "customfield_10273" )
				{
						var configXML:String = event.result.customFieldValues[i].values[0];
						var jiraConfigData:XML = new XML( configXML );
						break;
				}
			}

			displayMessage("Received project config data:");
			displayMessage( jiraConfigData.toString() );
			
			// This next bit is E4X.  Good luck understanding it.  Yuck.
			
			m_releasesAndMilestones = new Array;
			var projects:XMLList = jiraConfigData.project;
			for( i = 0; i < projects.length(); i++ )
			{
				m_releasesAndMilestones[i] = new Object;
				m_releasesAndMilestones[i].name = new String( jiraConfigData.project[i].@name );
				m_releasesAndMilestones[i].milestones = new Array;
				
				var milestones:XMLList = jiraConfigData.project[i].milestones.milestone;
				for( var j:uint = 0; j < milestones.length(); j++ )
					m_releasesAndMilestones[i].milestones[j] = new String( jiraConfigData.project[i].milestones.milestone[j].@name );
			}

			// Call the next operation in the loading sequence.
			getComponentList();
		}
		
		////////////////////////////////////////////////////////////////////////////////
		
		protected function getComponentList():void
		{
			displayMessage("Getting component list for " + jiraProject );
			
			var componentResponder:CallResponder = new CallResponder;
			componentResponder.addEventListener(ResultEvent.RESULT, handleComponentResult );
			componentResponder.addEventListener(FaultEvent.FAULT, defaultJiraFault );
			componentResponder.token = jiraService.getComponents( m_loginToken, m_jiraProject );
		}
		
		protected function handleComponentResult(event:ResultEvent):void
		{
			for( var i:uint = 0; i < event.result.length; i++ )
			{
				displayMessage("Received component name " +  event.result[i].name);
				m_workItemsByFeature[i] = new Object;
				m_workItemsByFeature[i].name = new String( event.result[i].name );
				m_workItemsByFeature[i].workers = new Array;
			}

			// Call the next operation in the loading sequence.
			getWorkItemGroups();
		}
		
		////////////////////////////////////////////////////////////////////////////////
		
		private function updateSearchQueries():void
		{
			m_workItemsGroupsQuery = m_workItemGroupsQueryTemplate.replace( "%%PROJ%%", m_jiraProject );
			m_workItemsPerGroupQuery = m_workItemsPerGroupQueryTemplate.replace( "%%PROJ%%", m_jiraProject );
			
			var milestones:String = new String;
			
			for( var i:uint = 0; i < m_releasesAndMilestones[m_currentReleaseIndex].milestones.length; i++ )
			{
				milestones = milestones.concat( "\"" );
				milestones = milestones.concat( m_releasesAndMilestones[m_currentReleaseIndex].milestones[i] );
				milestones = milestones.concat( "\"" );

				if( i != (m_releasesAndMilestones[m_currentReleaseIndex].milestones.length - 1) )
					milestones = milestones.concat( "," );
			}

			m_workItemsGroupsQuery = m_workItemsGroupsQuery.replace( "%%MILESTONES%%", milestones );
			m_workItemsPerGroupQuery = m_workItemsPerGroupQuery.replace( "%%MILESTONES%%", milestones );
		}
		
		private function getWorkItemGroups():void
		{
			updateSearchQueries();
			
			var featureResponder:CallResponder = new CallResponder;
			featureResponder.addEventListener(ResultEvent.RESULT, handleWorkItemGroupsResult );
			featureResponder.addEventListener(FaultEvent.FAULT, defaultJiraFault );
			
			featureResponder.token = jiraService.getIssuesFromJqlSearch( m_loginToken, m_workItemsGroupsQuery, m_maxQueryResults );
		}

		protected function handleWorkItemGroupsResult(event:ResultEvent):void
		{
			displayMessage("Retrieved " + event.result.length.toString() + " work item groups." );

			// If the feature query didn't return any results, then we can stop the server call sequence here.
			if( 0 == event.result.length )
			{
				m_operationInProgress = false;
				m_projectDataLoaded = true;
				dispatchChangeEvent();
				return;
			}

			// Walk through the work item groups, save off the information we need, and
			// issue a work items query for each group.

			var currentWorkItemGroup:uint = 0;
			var currentWorkItemsQuery:String;

			m_workItemQueriesParentTokenMap.length = 0;
			m_workItemQueriesOutstanding = 0;		

			for( var i:uint = 0; i < event.result.length; i++ )
			{
				m_workItemsByWorkItemGroup[currentWorkItemGroup] = new Object;
				m_workItemsByWorkItemGroup[currentWorkItemGroup].key = event.result[i].key;
				m_workItemsByWorkItemGroup[currentWorkItemGroup].summary = event.result[i].summary;
				m_workItemsByWorkItemGroup[currentWorkItemGroup].assignee = event.result[i].assignee;
				m_workItemsByWorkItemGroup[currentWorkItemGroup].work_items = new Array;

				currentWorkItemsQuery = m_workItemsPerGroupQuery.replace( "%%PARENT%%", event.result[i].key );

				var workitemsResponder:CallResponder = new CallResponder;
				workitemsResponder.addEventListener(ResultEvent.RESULT, handleWorkItemsResult );
				workitemsResponder.addEventListener(FaultEvent.FAULT, handleWorkItemsFault );
				
				workitemsResponder.token = jiraService.getIssuesFromJqlSearch( m_loginToken, currentWorkItemsQuery, m_maxQueryResults );
				m_workItemQueriesOutstanding++;
				
				m_workItemQueriesParentTokenMap[currentWorkItemGroup] = new Object;
				m_workItemQueriesParentTokenMap[currentWorkItemGroup].key = event.result[i].key;
				m_workItemQueriesParentTokenMap[currentWorkItemGroup].token = workitemsResponder.token; 
				
				currentWorkItemGroup++;
			}
		}	

		protected function handleWorkItemsFault(event:FaultEvent):void
		{
			Alert.show(event.fault.faultString);
			displayMessage( "Jira Fault Details: " + event.fault.faultDetail + "\n");

			postProcessWorkItemResponse();
		}

		protected function handleWorkItemsResult(event:ResultEvent):void
		{
			if( event.result.length > 0 )
			{
				// Find the right work item group to add the work items to.
				var workItems:Array;
				for( var i:uint = 0; i < m_workItemQueriesParentTokenMap.length; i++ )
				{
					if( m_workItemQueriesParentTokenMap[i].token == event.token )
					{
						for( var j:uint = 0; j < m_workItemsByWorkItemGroup.length; j++ )
						{
							if( m_workItemsByWorkItemGroup[j].key == m_workItemQueriesParentTokenMap[i].key )
							{
								workItems = m_workItemsByWorkItemGroup[j].work_items;
								displayMessage( "Retrieved " + event.result.length.toString() +
											    " work items for group " + m_workItemsByWorkItemGroup[j].key );
								break;
							}
						}
						break;
					}
				}

				// Add this work item to that list, and save off the days remaining.
				var currentDays:Number = 0;
				for( i = 0 ; i < event.result.length ; i++ )
				{
					workItems[event.result.length - 1 - i] = new Object;
					workItems[event.result.length - 1 - i].key = event.result[i].key; 
					workItems[event.result.length - 1 - i].assignee = event.result[i].assignee;
					
					for( j = 0; j < event.result[i].customFieldValues.length; j++ )
					{
						if( event.result[i].customFieldValues[j].customfieldId == "customfield_10271" )
						{
							currentDays = event.result[i].customFieldValues[j].values[0];
							workItems[event.result.length - 1 - i].days = currentDays; 
							break;
						}
					}

					// If this summary has the order encoded, decode it.
					if( isEncodedSummary( event.result[i].summary ) )
					{
						workItems[event.result.length - 1 - i].summary = getSummaryFromEncodedSummary( event.result[i].summary );
						workItems[event.result.length - 1 - i].ordinal = getOrdinalFromEncodedSummary( event.result[i].summary );
					}
					else
					{
						workItems[event.result.length - 1 - i].summary = event.result[i].summary;
						workItems[event.result.length - 1 - i].ordinal = NaN;
					}
					
					// Now add this work item to the summary data by component.
					if( event.result[i].components.length > 0 )
					{
						for( j = 0; j < m_workItemsByFeature.length; j++ )
						{
							if( m_workItemsByFeature[j].name == event.result[i].components[0].name )
							{
								var foundWorker:Boolean = false;
								for( var k:uint = 0; k < m_workItemsByFeature[j].workers.length; k++ )
								{
									if( m_workItemsByFeature[j].workers[k].assignee == event.result[i].assignee)
									{
										m_workItemsByFeature[j].workers[k].days += currentDays;
										foundWorker = true;
									}
								}
								if( false == foundWorker )
								{
									var newWorker:Object = new Object;
									newWorker.assignee = event.result[i].assignee;
									newWorker.days = currentDays;
									m_workItemsByFeature[j].workers[m_workItemsByFeature[j].workers.length] = newWorker;
								}
								break;
							}
						}
					}
				}
			}

			postProcessWorkItemResponse();
		}

		private function postProcessWorkItemResponse():void
		{
			m_workItemQueriesOutstanding--;
			
			// When we've processed the final work items query, we're done!
			if( 0 == m_workItemQueriesOutstanding )
			{
				m_operationInProgress = false;
				m_projectDataLoaded = true;
				dispatchChangeEvent();
			}
		}
		
		////////////////////////////////////////////////////////////////////////////////
		//
		// The pending updates queue.
		//
		
		private var m_pendingUpdates:Array = new Array;
		private var m_updatesOutstanding:Number;
		
		private function clearPendingUpdates():void { m_pendingUpdates.length = 0; }
		public function havePendingUpdates():Boolean { return( 0 != m_pendingUpdates.length ); }

		private function getPendingUpdateByKey(key:String):Object
		{
			for( var i:uint = 0; i < m_pendingUpdates.length; i++ )
			{
				if( m_pendingUpdates[i].key == key )
					return( m_pendingUpdates );
			}
			
			return( null );
		}
		
		public function updateWorkItemDays(key:String, days:Number):void
		{
			displayMessage( "Adding a pending update for issue " + key + " to " + days + " days." );
			
			var update:Object = getPendingUpdateByKey(key);
			if( update )
			{
				update.days = days;
			}
			else
			{
				var newUpdate:Object = new Object;
				newUpdate.key = key;
				newUpdate.days = days;
				m_pendingUpdates[m_pendingUpdates.length] = newUpdate;	
			}

			dispatchChangeEvent();
		}
		
		public function updateOrderedWorkItemSummary(key:String, ordinal:Number, summary:String):void
		{
			var update:Object = getPendingUpdateByKey(key);
			if( update )
			{
				update.summary = encodeOrdinalAndSummary( ordinal, summary );
			}
			else
			{
				var newUpdate:Object = new Object;
				newUpdate.key = key;
				newUpdate.summary = encodeOrdinalAndSummary( ordinal, summary );
				m_pendingUpdates[m_pendingUpdates.length] = newUpdate;
				update = newUpdate;
			}

			displayMessage( "Adding a pending update for issue " + key + " to encoded summary: " + update.summary );
			dispatchChangeEvent();
		}
		
		private function encodeOrdinalAndSummary(ordinal:Number, summary:String):String
		{
			var encoded:String = new String;
			
			encoded += "<<";
			encoded += ordinal.toString();
			encoded += ">>:";
			encoded += summary;
			
			return( encoded );
		}
		
		private function getOrdinalFromEncodedSummary(encodedSummary:String):Number
		{
			if( false == isEncodedSummary(encodedSummary) )
				return( NaN );
			
			var endPosition:uint = encodedSummary.indexOf(">>:");
			var ordinal:String = encodedSummary.slice(2,endPosition);
			return( parseFloat( ordinal ) );
		}
		
		private function getSummaryFromEncodedSummary(encodedSummary:String):String
		{
			if( false == isEncodedSummary(encodedSummary) )
				return( encodedSummary );
			
			var endPosition:uint = encodedSummary.indexOf(">>:");
			var summary:String = encodedSummary.slice(endPosition+3);
			return( summary );
		}
		
		private function isEncodedSummary(encodedSummary:String):Boolean
		{
			if( ( encodedSummary.charAt(0) == "<" ) && ( encodedSummary.charAt(1) == "<" ) )
			{
				var endPosition:uint = encodedSummary.indexOf(">>:");
				if( -1 != endPosition )
				{
					var ordinalString:String = encodedSummary.slice(2,endPosition);
					var ordinal:Number = parseFloat(ordinalString);
					if( false == isNaN(ordinal) )
						return( true );
				}
			}
			
			return( false );
		}
		
		////////////////////////////////////////////////////////////////////////////////

		public function commitPendingUpdates():void
		{
			// You cannot initiate this operation if there's already some operation in progress.
			if( m_operationInProgress ) return;
			
			// You cannot initiate this request if you are not logged in.
			if( null == m_loginToken ) return;
			
			m_operationInProgress = true;
			
			displayMessage( "Initiating pending updates." );
			m_updatesOutstanding = 0;
			
			for( var i:uint = 0; i < m_pendingUpdates.length; i++ )
			{
				if( null != m_pendingUpdates[i].days )
				{
					var daysResponder:CallResponder = new CallResponder;
					daysResponder.addEventListener(ResultEvent.RESULT, handleCommitResult );
					daysResponder.addEventListener(FaultEvent.FAULT, handleCommitFault );
									
					// This call takes an array of updates. Each update has an id and an array of values.
					// Ask me how long it took to figure this out.  Blech.
	
					var daysField:RemoteFieldValue = new RemoteFieldValue;
					daysField.id = "customfield_10271";
					daysField.values = new ArrayCollection( new Array( m_pendingUpdates[i].days.toString() ) );
					var daysUpdateList:ArrayCollection = new ArrayCollection( new Array( daysField ) );
	
					daysResponder.token = jiraService.updateIssue( m_loginToken, m_pendingUpdates[i].key, daysUpdateList );
					m_updatesOutstanding++;
				}

				if( null != m_pendingUpdates[i].summary )
				{
					var summaryResponder:CallResponder = new CallResponder;
					summaryResponder.addEventListener(ResultEvent.RESULT, handleCommitResult );
					summaryResponder.addEventListener(FaultEvent.FAULT, handleCommitFault );
					
					// This call takes an array of updates. Each update has an id and an array of values.
					// Ask me how long it took to figure this out.  Blech.
					
					var summaryField:RemoteFieldValue = new RemoteFieldValue;
					summaryField.id = "summary";
					summaryField.values = new ArrayCollection( new Array( m_pendingUpdates[i].summary.toString() ) );
					var summaryUpdateList:ArrayCollection = new ArrayCollection( new Array( summaryField ) );
					
					summaryResponder.token = jiraService.updateIssue( m_loginToken, m_pendingUpdates[i].key, summaryUpdateList );
					m_updatesOutstanding++;
				}

			}
		}

		protected function handleCommitResult(event:ResultEvent):void
		{
			postProcessUpdateResult();
		}
		
		protected function handleCommitFault(event:FaultEvent):void
		{
			defaultJiraFault(event);
			postProcessUpdateResult();
		}

		private function postProcessUpdateResult():void
		{
			displayMessage( "Processing update result." );
			m_updatesOutstanding--;

			if( 0 == m_updatesOutstanding )
			{
				displayMessage( "All updates completed." );
				m_operationInProgress = false;
				m_pendingUpdates.length = 0;
				dispatchChangeEvent();
			}
		}

	}
}

class SingletonEnforcer { }